package io.github.wanggit.antrpc.client.zk.zknode;

import io.github.wanggit.antrpc.client.zk.exception.InterfaceProviderNotFoundException;
import io.github.wanggit.antrpc.client.zk.lb.ILoadBalancer;
import io.github.wanggit.antrpc.client.zk.lb.ILoadBalancerContainer;
import io.github.wanggit.antrpc.commons.bean.*;
import io.github.wanggit.antrpc.commons.constants.ConstantValues;
import io.github.wanggit.antrpc.commons.test.ForTest;
import io.github.wanggit.antrpc.commons.utils.JsonUtils;
import io.github.wanggit.antrpc.commons.utils.Utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.utils.ZKPaths;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Slf4j
public final class NodeHostContainer implements INodeHostContainer {

    private final ILoadBalancerContainer loadBalancerContainer;
    private final IZkNodeOperator zkNodeOperator;
    private final Map<String, DirectNodeHostEntity> directHosts;
    private final ConcurrentHashMap<String, String> ipAndAppNameMaps = new ConcurrentHashMap<>();

    public NodeHostContainer(
            ILoadBalancerContainer loadBalancerContainer,
            Map<String, DirectNodeHostEntity> directHosts,
            IZkNodeOperator zkNodeOperator) {
        this.loadBalancerContainer = loadBalancerContainer;
        this.directHosts = directHosts;
        this.zkNodeOperator = zkNodeOperator;
    }

    @Override
    public void addOrUpdateIpAndAppNamePair(String hostInfo, String appName) {
        if (log.isDebugEnabled()) {
            log.debug("save " + hostInfo + " -> " + appName + " to ipAndAppNameMaps.");
        }
        ipAndAppNameMaps.put(hostInfo, appName);
    }

    @Override
    public void deleteIpAndAppNamePair(String hostInfo) {
        if (log.isDebugEnabled()) {
            log.debug("save " + hostInfo + " from ipAndAppNameMaps.");
        }
        ipAndAppNameMaps.remove(hostInfo);
    }

    @Override
    public Map<String, List<NodeHostEntity>> entitiesSnapshot() {
        return loadBalancerContainer.getAllNodeHostEntities();
    }

    @ForTest
    @Override
    public List<NodeHostEntity> getHostEntities(String className, String methodFullName) {
        if (null == className || null == methodFullName) {
            throw new IllegalArgumentException("className and methodFullName cannot be null.");
        }
        String classMethodFullName = Utils.classMethodFullName(className, methodFullName);
        return loadBalancerContainer.getLoadBalancer(classMethodFullName).getAllNodeHostEntities();
    }

    @Override
    public NodeHostEntity choose(String className, String methodFullName) {
        if (null == className || null == methodFullName) {
            throw new IllegalArgumentException("className and methodFullName cannot be null.");
        }
        // 直连时只依据接口判断
        if (null != directHosts && !directHosts.isEmpty()) {
            DirectNodeHostEntity directNodeHostEntity = directHosts.get(className);
            if (null != directNodeHostEntity) {
                if (log.isWarnEnabled()) {
                    log.warn(
                            "The "
                                    + className
                                    + " interface is directly connected to "
                                    + directNodeHostEntity.getHostInfo()
                                    + ", "
                                    + "and this configuration cannot be turned on in a production environment.");
                }
                return directNodeHostEntity;
            }
        }
        String fullName = Utils.classMethodFullName(className, methodFullName);
        ILoadBalancer loadBalancer = loadBalancerContainer.getLoadBalancer(fullName);
        if (null == loadBalancer || loadBalancer.isEmpty()) {
            throw new InterfaceProviderNotFoundException(
                    "No service provider for the "
                            + className
                            + "#"
                            + methodFullName
                            + " interface was found.");
        }
        NodeHostEntity choosed = loadBalancer.choose();
        if (log.isDebugEnabled()) {
            log.debug(
                    fullName
                            + "#"
                            + methodFullName
                            + " --> choosed="
                            + JsonUtils.toJsonString(choosed)
                            + " \n loadBalancer="
                            + loadBalancer.getClass().getName());
        }
        // 因为节点监听关系，可能此字段为空，所以需要重新设置一次AppName
        if (null == choosed.getAppName()) {
            choosed.setAppName(ipAndAppNameMaps.get(choosed.getHostInfo()));
        }
        return choosed;
    }

    @Override
    public void add(String className, NodeHostEntity nodeHostEntity) {
        if (null == className || null == nodeHostEntity) {
            throw new IllegalArgumentException("className and nodeHostEntity cannot be null.");
        }
        synchronized (className.intern()) {
            Map<String, RegisterBeanMethod> methodMap = nodeHostEntity.getMethodMap();
            methodMap.forEach(
                    (key, value) ->
                            internalLoadBalancerUpdateHost(className, nodeHostEntity, value));
        }
    }

    @Override
    public void update(String className, NodeHostEntity nodeHostEntity) {
        if (null == className || null == nodeHostEntity) {
            throw new IllegalArgumentException("className and nodeHostEntity cannot be null.");
        }
        synchronized (className.intern()) {
            internalUpdate(className, nodeHostEntity);
        }
    }

    private void internalUpdate(String className, NodeHostEntity nodeHostEntity) {
        Map<String, RegisterBeanMethod> methodMap = nodeHostEntity.getMethodMap();
        methodMap.forEach(
                (key, value) -> internalLoadBalancerUpdateHost(className, nodeHostEntity, value));
    }

    private void internalLoadBalancerUpdateHost(
            String className, NodeHostEntity nodeHostEntity, RegisterBeanMethod value) {
        String fullName = Utils.classMethodFullName(className, value.toString());
        nodeHostEntity.setAppName(ipAndAppNameMaps.get(nodeHostEntity.getHostInfo()));
        loadBalancerContainer.updateHost(fullName, nodeHostEntity);
        if (log.isDebugEnabled()) {
            log.debug(
                    "Save or Update to LoadBalancerContainer. className="
                            + fullName
                            + " \n nodeHostEntity="
                            + JsonUtils.toJsonPrettyString(nodeHostEntity));
        }
    }

    @Override
    public void delete(String className, NodeHostEntity nodeHostEntity) {
        if (null == className || null == nodeHostEntity) {
            throw new IllegalArgumentException("className and nodeHostEntity cannot be null.");
        }
        synchronized (className.intern()) {
            internalDelete(className, nodeHostEntity);
        }
    }

    @Override
    public void checkLocalCacheNodes(Set<SubscribeNode> subscribeNodes) {
        if (null == subscribeNodes || subscribeNodes.isEmpty()) {
            return;
        }
        Set<String> classNames =
                subscribeNodes.stream()
                        .map(SubscribeNode::getClassName)
                        .collect(Collectors.toSet());
        if (log.isDebugEnabled()) {
            log.debug("Check Local Cache, classNames=" + JsonUtils.toJsonPrettyString(classNames));
        }
        List<String> remoteNodes = new ArrayList<>();
        zkNodeOperator.getDescendants(
                "/" + ConstantValues.ZK_ROOT_NODE_NAME,
                new IZkNodeOperator.Filter() {
                    @Override
                    public boolean accepted(String path) {
                        List<String> tmps = ZKPaths.split(path);
                        return classNames.contains(tmps.get(tmps.size() - 1));
                    }
                },
                remoteNodes);
        if (log.isDebugEnabled()) {
            log.debug("All RemoteNodes: " + JsonUtils.toJsonPrettyString(remoteNodes));
        }
        Map<String, List<NodeInfo>> hosts = new HashMap<>();
        remoteNodes.forEach(
                rn -> {
                    List<String> ps = ZKPaths.split(rn);
                    String className = ps.get(ps.size() - 1);
                    if (!hosts.containsKey(className)) {
                        hosts.put(className, new ArrayList<>());
                    }
                    hosts.get(className).add(new NodeInfo(ps.get(1), rn));
                });
        hosts.forEach(this::internalCheckLocalCacheNode);
    }

    private void internalCheckLocalCacheNode(String className, List<NodeInfo> nodeInfos) {
        // 检查从这个类中选择一个方法进行检测
        RegisterBeanMethod[] registerBeanMethods =
                RegisterBeanHelper.findMethodsWithoutJavaBasicMethods(className);
        if (null != registerBeanMethods && registerBeanMethods.length > 0) {
            List<NodeHostEntity> entities =
                    getHostEntities(className, registerBeanMethods[0].toString());
            Set<String> localCachedHostInfo =
                    entities.stream().map(Host::getHostInfo).collect(Collectors.toSet());
            Set<NodeInfo> infos = NodeInfo.removeAll(nodeInfos, localCachedHostInfo);
            if (log.isDebugEnabled()) {
                log.debug("RemoveAll Result=" + JsonUtils.toJsonPrettyString(infos));
            }
            if (!infos.isEmpty()) {
                // 本地缓存与远程不匹配
                infos.forEach(
                        it -> {
                            byte[] bytes = zkNodeOperator.getData(it.getFullpath());
                            InterfaceNodeDataBean nodeData =
                                    InterfaceNodeDataBeanHelper.getNodeDataWithoutRateLimiting(
                                            bytes);
                            NodeHostEntity hostEntity =
                                    NodeHostEntityHelper.createNodeHostEntity(
                                            it.fullpath, nodeData);
                            add(hostEntity.getClassName(), hostEntity);
                        });
            }
        }
    }

    private void internalDelete(String className, NodeHostEntity nodeHostEntity) {
        Map<String, RegisterBeanMethod> methodMap = nodeHostEntity.getMethodMap();
        methodMap.forEach(
                (key, value) -> {
                    String fullName = Utils.classMethodFullName(className, value.toString());
                    loadBalancerContainer.deleteHost(fullName, nodeHostEntity);
                    if (log.isDebugEnabled()) {
                        log.debug(
                                "Delete the "
                                        + nodeHostEntity.getHostInfo()
                                        + " node of the "
                                        + fullName
                                        + ".");
                    }
                });
    }

    static class NodeInfo {
        private final String hostInfo;
        private final String fullpath;

        public NodeInfo(String hostInfo, String fullpath) {
            this.hostInfo = hostInfo;
            this.fullpath = fullpath;
        }

        public String getHostInfo() {
            return hostInfo;
        }

        public String getFullpath() {
            return fullpath;
        }

        public static Set<NodeInfo> removeAll(List<NodeInfo> nodeInfos, Set<String> locales) {
            Map<String, NodeInfo> tmps =
                    nodeInfos.stream().collect(Collectors.toMap(NodeInfo::getHostInfo, it -> it));
            locales.forEach(tmps::remove);
            return new HashSet<>(tmps.values());
        }
    }
}
